package com.ihr360.job.core.repository.support;

import com.ihr360.job.core.repository.JobRepository;
import com.ihr360.job.core.repository.SimpleJobRepository;
import com.ihr360.job.core.repository.dao.ExecutionContextDao;
import com.ihr360.job.core.repository.dao.JobExecutionDao;
import com.ihr360.job.core.repository.dao.JobInstanceDao;
import com.ihr360.job.core.repository.dao.StepExecutionDao;
import com.ihr360.job.core.repository.dao.StepExecutionLogDao;
import com.ihr360.job.core.support.PropertiesConverter;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;

/**
 * A {@link FactoryBean} that automates the creation of a
 * {@link SimpleJobRepository}. Declares abstract methods for providing DAO
 * object implementations.
 *
 * @see JobRepositoryFactoryBean
 * @see MapJobRepositoryFactoryBean
 *
 * @author Ben Hale
 * @author Lucas Ward
 * @author Robert Kasanicky
 */
public abstract class AbstractJobRepositoryFactoryBean implements FactoryBean<JobRepository>, InitializingBean {

    private PlatformTransactionManager transactionManager;

    private ProxyFactory proxyFactory;

    private String isolationLevelForCreate = DEFAULT_ISOLATION_LEVEL;

    private boolean validateTransactionState = true;

    /**
     * Default value for isolation level in create* method.
     */
    private static final String DEFAULT_ISOLATION_LEVEL = "ISOLATION_SERIALIZABLE";

    /**
     * @return fully configured {@link JobInstanceDao} implementation.
     */
    protected abstract JobInstanceDao createJobInstanceDao() throws Exception;

    /**
     * @return fully configured {@link JobExecutionDao} implementation.
     */
    protected abstract JobExecutionDao createJobExecutionDao() throws Exception;

    /**
     * @return fully configured {@link StepExecutionDao} implementation.
     */
    protected abstract StepExecutionDao createStepExecutionDao() throws Exception;

    protected abstract StepExecutionLogDao createStepExecutionLogDao() throws Exception;

    /**
     * @return fully configured {@link ExecutionContextDao} implementation.
     */
    protected abstract ExecutionContextDao createExecutionContextDao() throws Exception;

    /**
     * The type of object to be returned from {@link #getObject()}.
     *
     * @return JobRepository.class
     * @see org.springframework.beans.factory.FactoryBean#getObjectType()
     */
    @Override
    public Class<JobRepository> getObjectType() {
        return JobRepository.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    /**
     * Flag to determine whether to check for an existing transaction when a
     * JobExecution is created. Defaults to true because it is usually a
     * mistake, and leads to problems with restartability and also to deadlocks
     * in multi-threaded steps.
     *
     * @param validateTransactionState the flag to set
     */
    public void setValidateTransactionState(boolean validateTransactionState) {
        this.validateTransactionState = validateTransactionState;
    }

    /**
     * public setter for the isolation level to be used for the transaction when
     * job execution entities are initially created. The default is
     * ISOLATION_SERIALIZABLE, which prevents accidental concurrent execution of
     * the same job (ISOLATION_REPEATABLE_READ would work as well).
     *
     * @param isolationLevelForCreate the isolation level name to set
     *
     * @see SimpleJobRepository#createJobExecution(String,
     * com.ihr360.job.core.JobParameters)
     */
    public void setIsolationLevelForCreate(String isolationLevelForCreate) {
        this.isolationLevelForCreate = isolationLevelForCreate;
    }

    /**
     * Public setter for the {@link PlatformTransactionManager}.
     * @param transactionManager the transactionManager to set
     */
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    /**
     * The transaction manager used in this factory. Useful to inject into steps
     * and jobs, to ensure that they are using the same instance.
     *
     * @return the transactionManager
     */
    public PlatformTransactionManager getTransactionManager() {
        return transactionManager;
    }

    /**
     * Convenience method for clients to grab the {@link JobRepository} without
     * a cast.
     * @return the {@link JobRepository} from {@link #getObject()}
     * @throws Exception if the repository could not be created
     * @deprecated use {@link #getObject()} instead
     */
    public JobRepository getJobRepository() throws Exception {
        return getObject();
    }

    private void initializeProxy() throws Exception {
        if (proxyFactory == null) {
            proxyFactory = new ProxyFactory();
            TransactionInterceptor advice = new TransactionInterceptor(transactionManager,
                    PropertiesConverter.stringToProperties("create*=PROPAGATION_REQUIRES_NEW,"
                            + isolationLevelForCreate + "\ngetLastJobExecution*=PROPAGATION_REQUIRES_NEW,"
                            + isolationLevelForCreate + "\n*=PROPAGATION_REQUIRED"));
            if (validateTransactionState) {
                DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MethodInterceptor() {
                    @Override
                    public Object invoke(MethodInvocation invocation) throws Throwable {
                        if (TransactionSynchronizationManager.isActualTransactionActive()) {
                            throw new IllegalStateException(
                                    "Existing transaction detected in JobRepository. "
                                            + "Please fix this and try again (e.g. remove @Transactional annotations from client).");
                        }
                        return invocation.proceed();
                    }
                });
                NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
                pointcut.addMethodName("create*");
                advisor.setPointcut(pointcut);
                proxyFactory.addAdvisor(advisor);
            }
            proxyFactory.addAdvice(advice);
            proxyFactory.setProxyTargetClass(false);
            proxyFactory.addInterface(JobRepository.class);
            proxyFactory.setTarget(getTarget());
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(transactionManager, "TransactionManager must not be null.");

        initializeProxy();
    }

    private Object getTarget() throws Exception {
        return new SimpleJobRepository(createJobInstanceDao(), createJobExecutionDao(), createStepExecutionDao(),
                createExecutionContextDao(),createStepExecutionLogDao());
    }

    @Override
    public JobRepository getObject() throws Exception {
        if (proxyFactory == null) {
            afterPropertiesSet();
        }
        return (JobRepository) proxyFactory.getProxy();
    }

}
